package cz.vse.po1715.lode.net.server;

import cz.vse.po1715.lode.net.obecne.ISHrac;
import cz.vse.po1715.lode.net.obecne.Příloha;
import cz.vse.po1715.lode.net.obecne.IHerniUdalost;
import java.nio.*;
import java.nio.channels.*;
import java.io.*;
import java.util.*;
import java.util.logging.*;

/*******************************************************************************
 * Instance třídy {@code VyberANačti} představují načítáče událostí od všech
 * připojených klientů a předávají herní události příslušnému správci hry
 *
 * @author    Dominik KOSTKA
 * @version   0.00.000
 */
public class VyberANačti extends Thread
{
//== KONSTANTNÍ ATRIBUTY TŘÍDY =================================================
//== PROMĚNNÉ ATRIBUTY TŘÍDY ===================================================

    /** logger */
    private static Logger log = Logger.getLogger("VyberANačti");
//== STATICKÝ INICIALIZAČNÍ BLOK - STATICKÝ KONSTRUKTOR ========================
//== KONSTANTNÍ ATRIBUTY INSTANCÍ ==============================================
//== PROMĚNNÉ ATRIBUTY INSTANCÍ ================================================

    /** klienti čekající na připojení */
    private LinkedList novíKlienti;

    //    /** přestávka pro volání metody select() selectoru*/
    //    private static final long SELECT_TIMEOUT = 250;

    /** selector multiplexující přístup ke klientským kanálům */
    private Selector selector;

    /** odkaz na spuštěný herní server */
    private Server server;

//== PŘÍSTUPOVÉ METODY VLASTNOSTÍ TŘÍDY ========================================
//== OSTATNÍ NESOUKROMÉ METODY TŘÍDY ===========================================

//##############################################################################
//== KONSTRUKTORY A TOVÁRNÍ METODY =============================================

    /***************************************************************************
     *
     */
    public VyberANačti(Server server)
    {
        this.server = server;
	novíKlienti = new LinkedList();
    }



//== ABSTRAKTNÍ METODY =========================================================
//== PŘÍSTUPOVÉ METODY VLASTNOSTÍ INSTANCÍ =====================================
//== OSTATNÍ NESOUKROMÉ METODY INSTANCÍ ========================================

    /**
     * přidá kanál klienta do seznamu klientů, čekajících na připojení
     */
    public void přidejNovéhoKlienta(SocketChannel kanálKlienta) {
	synchronized (novíKlienti) {
	    novíKlienti.addLast(kanálKlienta);
	}
	// donutí selector k vrácení z jeho metody select(),
	// takže náš nový klient může vstoupit do cyklu autentizace
        // připojení okamžitě
	selector.wakeup();
    }

    /**
     * provádí nekonečný cyklus, poprvé provedením metody vyber() a pak
     * zkontroluje nová připojení klientů čekající na vyřízení
     */
    @Override
    public void run () {
	try {
	    selector = Selector.open();

	    while (true) {
		vyber();
		zkontrolujNováPřipojení();

		// vlákno na chvíli usne
		try { Thread.sleep(30); } catch (InterruptedException e) {}
	    }
	}
	catch (IOException ioe) {
	    log.info("výjimka během otvírání selectoru");
      	}
    }

//== SOUKROMÉ A POMOCNÉ METODY TŘÍDY ===========================================
//== SOUKROMÉ A POMOCNÉ METODY INSTANCÍ ========================================

    /**
     * zkontroluje nová připojení
     * a registruje je se selectorem
     */
    private void zkontrolujNováPřipojení() {
	synchronized(novíKlienti) {
	    while (novíKlienti.size() > 0) {
		try {
		    SocketChannel kanálKlienta = (SocketChannel)novíKlienti.removeFirst();
		    kanálKlienta.configureBlocking( false);
		    kanálKlienta.register( selector, SelectionKey.OP_READ, new Příloha());
		}
		catch (ClosedChannelException cce) {
		    log.info("kanál uzavřen");
		}
		catch (IOException ioe) {
		    log.info("ioexception na kanále klienta");
		}
	    }
	}
    }

    /**
     * Vybere připravený klientský kanál a načítá z něj surová data události a
     * vytvoří z nich přílohu.
     * Pomocí read bufferu přílohy zkontroluje úplnost načtených dat události.
     * Kompletní událost načte do bufferu a z něj pak přímo do připravené
     * instance herní události
     */
    private void vyber() {
	try {
	    // toto je volání blokující metody select() ale bude
	    // přerušeno když přicházejí noví klienti
	    selector.select();
	    Set připravenéKlíče = selector.selectedKeys();

	    Iterator i = připravenéKlíče.iterator();
	    while (i.hasNext()) {
		SelectionKey key = (SelectionKey) i.next();
		i.remove();
		SocketChannel kanál = (SocketChannel) key.channel();
		Příloha příloha = (Příloha) key.attachment();

		try {
                    // načítá z klientského kanálu
		    long načteno = kanál.read(příloha.readBuff);

                    // zkontroluje podmínky konce proudu klientského kanálu
		    if (načteno == -1) {
			log.info("odpojení: " + kanál.socket().getInetAddress() +
				 ", konec datového proudu klientského kanálu");
			kanál.close();
		    }

		    // zkontroluje, zda jsou data události v příloze kompletní
		    try {
			if (příloha.readBuff.position() >=
                                Příloha.VELIKOST_HLAVIČKY) {
			    příloha.readBuff.flip();

			    // načte tolik událostí kolik jich je dostupných v
                            //bufferu
			    while(příloha.událostPřipravena()) {
				IHerniUdalost událost = getUdálost(příloha);
				předejUdálost(událost, kanál);
				příloha.vymaž();
			    }
			    // připraví se na další načítání z kanálu
			    příloha.readBuff.compact();
			}
		    }
		    catch (IllegalArgumentException e) {
			log.info("nepovolená výjimka argumentu");
		    }
		}
		catch (IOException ioe) {
		    log.warning("IOException během read(), uzavírání kanálu:"
                            + kanál.socket().getInetAddress());
		    kanál.close();
		}
	    }
	}
	catch (IOException ioe2) {
	    log.warning("IOException během select(): " + ioe2.getMessage());
	}
 	catch (Exception e) {
 	    log.info("exception během select()");
 	}
    }

    /**
     * načte událost z dat události v příloze
     */
    private IHerniUdalost getUdálost(Příloha příloha) {
	ByteBuffer bb = ByteBuffer.wrap(příloha.dataUdálosti);

	// get the controller and tell it to instantiate an event for us
	ASprávceHry správceHry = server.getSprávceHryPodleHash(příloha.hashKódHry);
	if (správceHry == null) {
	    return null;
	}
	IHerniUdalost událost = správceHry.vytvorHerniUdalost();

	// read the event from the payload
	událost.načti(bb);
	return událost;
    }

    /**
     * Na základě názvu hry získá odkaz na správce hry a předá mu herní událost
     * ke správě
     */
    private void předejUdálost(IHerniUdalost událost, SocketChannel kanál) {
	if (událost != null && událost.getNázevHry() == null) {
	    log.info("Nebyl zadán název hry.");
	    return;
	}

	ASprávceHry správceHry = server.getSprávceHry(událost.getNázevHry());
	if (správceHry == null) {
	    log.info("Žádný správce hry pro tuto hru: " + událost.getNázevHry());
	    return;
	}

	ISHrac hráč = Server.getHracPodleId(událost.getIdHrac());
	if (hráč != null) {
	    if (hráč.getKanál() != kanál) {
		log.warning("hráč je na novém kanále, musí být opětovně připojen.");
		hráč.setKanál(kanál);
	    }
	}
	else {
	    // jakmile  objevíme Id nového hráče, vytváříme instanci hráče,
	    // nastavíme klientský kanál a id hráče a přidáme hráče a jeho id do
            // hash tabulek hráčů na serveru
	    hráč = správceHry.vytvorHrac();
	    hráč.setIdHráč(událost.getIdHrac());
	    hráč.setKanál(kanál);
	    Server.přidejHráče(hráč);
	    log.info("Vytváření nového hráče a nastavení jeho klientského "
                    + "kanálu\nhráč: " + hráč.getIdHrac() + ", kanál: " + kanál);
	}

	správceHry.spravujUdálost(událost);
    }

//== INTERNÍ DATOVÉ TYPY =======================================================
//== TESTOVACÍ METODY A TŘÍDY ==================================================
//
//    /***************************************************************************
//     * Testovací metoda.
//     */
//    public static void test()
//    {
//        VyberANačti inst = new VyberANačti();
//    }
//    /** @param args Parametry příkazového řádku - nepoužívané. */
//    public static void main( String[] args )  {  test();  }
}
